package edu.northwestern.cbits.purple_robot_manager.probes.services; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.Map; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.logging.SanityCheck; import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; import edu.northwestern.cbits.xsi.oauth.FitbitBetaApi; import edu.northwestern.cbits.xsi.oauth.Keystore; import edu.northwestern.cbits.xsi.oauth.OAuthActivity; public class FitbitBetaProbe extends Probe { public final static String PROBE_NAME = "edu.northwestern.cbits.purple_robot_manager.probes.services.FitbitBetaProbe"; public static final String COVERED_RANGES = "fitbit-beta_covered_ranges"; protected static final String STEPS = "STEPS"; public static final String STEP_TIMESTAMPS = "STEP_TIMESTAMPS"; protected static final String CALORIES = "CALORIES"; public static final String CALORIES_TIMESTAMPS = "CALORIES_TIMESTAMPS"; protected static final String ELEVATION = "ELEVATION"; public static final String ELEVATION_TIMESTAMPS = "ELEVATION_TIMESTAMPS"; protected static final String DISTANCE = "DISTANCE"; public static final String DISTANCE_TIMESTAMPS = "DISTANCE_TIMESTAMPS"; protected static final String FLOORS = "FLOORS"; public static final String FLOORS_TIMESTAMPS = "FLOORS_TIMESTAMPS"; protected static final String HEART = "HEART"; public static final String HEART_TIMESTAMPS = "HEART_TIMESTAMPS"; private static final String ENABLED = "config_feature_fitbit_beta_probe_enabled"; private static final boolean DEFAULT_ENABLED = false; private static final String OAUTH_ACCESS_TOKEN = "oauth_fitbit-beta_access_token"; private static final String OAUTH_REFRESH_TOKEN = "oauth_fitbit-beta_refresh_token"; private static final String OAUTH_TOKEN_EXPIRES = "oauth_fitbit-beta_expires"; private static final String ENABLE_STEPS = "config_fitbit-beta_enable_distance"; private static final boolean DEFAULT_ENABLE_STEPS = true; private static final String ENABLE_CALORIES = "config_fitbit-beta_enable_calories"; private static final boolean DEFAULT_ENABLE_CALORIES = false; private static final String ENABLE_DISTANCE = "config_fitbit-beta_enable_distance"; private static final boolean DEFAULT_ENABLE_DISTANCE = true; private static final String ENABLE_FLOORS = "config_fitbit-beta_enable_floors"; private static final boolean DEFAULT_ENABLE_FLOORS = true; private static final String ENABLE_HEART = "config_fitbit-beta_enable_heart"; private static final boolean DEFAULT_ENABLE_HEART = false; private static final String ENABLE_ELEVATION = "config_fitbit-beta_enable_elevation"; private static final boolean DEFAULT_ENABLE_ELEVATION = false; private static final String RETROSPECTIVE_PERIOD = "config_fitbit-beta_retrospective_period"; private static final String DEFAULT_RETROSPECTIVE_PERIOD = "7"; // private static final String LAST_STEP_TIMESTAMP = "config_fitbit-beta_last_step_timestamp"; // private static final String LAST_HEART_TIMESTAMP = "config_fitbit-beta_last_heart_timestamp"; private static final String LAST_CALORIE_TIMESTAMP = "config_fitbit-beta_last_calorie_timestamp"; // private static final String LAST_FLOOR_TIMESTAMP = "config_fitbit-beta_last_floor_timestamp"; // private static final String LAST_DISTANCE_TIMESTAMP = "config_fitbit-beta_last_distance_timestamp"; // private static final String LAST_ELEVATION_TIMESTAMP = "config_fitbit-beta_last_elevation_timestamp"; private long _lastUpdate = 0; @Override public String getPreferenceKey() { return "services_fitbit_beta"; } public static class MinuteRange { public long start = 0; public long end = 0; public MinuteRange(long minute) { this.start = minute; this.end = minute; } public MinuteRange(long start, long end) { this.start = start; this.end = end; } public static MinuteRange rangeFromString(String token) { if (token.contains("-")) { String[] tokens = token.split("-"); return new MinuteRange(Long.parseLong(tokens[0]), Long.parseLong(tokens[1])); } return new MinuteRange(Long.parseLong(token)); } public String toString() { if (start == end) return "" + start; return (start + "-" + end); } public boolean containsMinute(long minute) { return ((this.start) <= minute && (this.end >= minute)); } public boolean overlaps(MinuteRange range) { if (this.start >= range.start && this.start <= range.end) return true; else if (this.end >= range.start && this.end <= range.end) return true; else if (range.start >= this.start && range.start <= this.end) return true; else if (range.end >= this.start && range.end <= this.end) return true; return false; } public void merge(MinuteRange range) { if (range.start < this.start) this.start = range.start; if (range.end > this.end) this.end = range.end; } } public static class MinuteRanges { ArrayList<MinuteRange> ranges = new ArrayList<>(); public void addMinute(long minute) { for (MinuteRange range : this.ranges) { if (range.start - 1 == minute) { range.start = minute; return; } if (range.end + 1 == minute) { range.end = minute; return; } } this.ranges.add(new MinuteRange(minute)); } public void addRange(MinuteRange range) { this.ranges.add(range); } public boolean containsMinute(long minute) { for (MinuteRange range : this.ranges) { if (range.containsMinute(minute)) return true; } return false; } public String coalescedJSON() { Collections.sort(this.ranges, new Comparator<MinuteRange>() { @Override public int compare(MinuteRange one, MinuteRange two) { if (one.start < two.start) return -1; else if (one.start > two.start) return 1; return 0; } }); ArrayList<MinuteRange> newRanges = new ArrayList<>(); for (MinuteRange range : this.ranges) { boolean add = true; for (MinuteRange newRange : newRanges) { if (newRange.overlaps(range)) { newRange.merge(range); add = false; } } if (add) newRanges.add(range); } JSONArray jsonArray = new JSONArray(); for (MinuteRange newRange : newRanges) { jsonArray.put(newRange.toString()); } int oldCount = this.ranges.size(); int newCount = newRanges.size(); this.ranges = newRanges; if (oldCount != newCount) return this.coalescedJSON(); return jsonArray.toString(); } } @Override public String summary(Context context) { return context.getString(R.string.summary_fitbit_beta_probe_desc); } @Override public String probeCategory(Context context) { return context.getResources().getString(R.string.probe_beta_category); } @Override public String name(Context context) { return "edu.northwestern.cbits.purple_robot_manager.probes.services.FitbitBetaProbe"; } @Override public String title(Context context) { return context.getString(R.string.title_fitbit_beta_probe); } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(FitbitBetaProbe.ENABLED, true); e.commit(); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(FitbitBetaProbe.ENABLED, false); e.commit(); } private void initKeystore(Context context) { Keystore.put(FitbitBetaApi.OAUTH2_CLIENT_ID, context.getString(R.string.fitbit_client_id)); Keystore.put(FitbitBetaApi.CONSUMER_KEY, context.getString(R.string.fitbit_consumer_key)); Keystore.put(FitbitBetaApi.CONSUMER_SECRET, context.getString(R.string.fitbit_consumer_secret)); Keystore.put(FitbitBetaApi.CALLBACK_URL, "http://purple.robot.com/oauth/fitbit-beta"); } @Override public boolean isEnabled(final Context context) { final SharedPreferences prefs = Probe.getPreferences(context); if (super.isEnabled(context)) { if (prefs.getBoolean(FitbitBetaProbe.ENABLED, false)) { this.initKeystore(context); String token = prefs.getString(FitbitBetaProbe.OAUTH_ACCESS_TOKEN, ""); String refresh = prefs.getString(FitbitBetaProbe.OAUTH_REFRESH_TOKEN, ""); final String title = context.getString(R.string.title_fitbit_check); final SanityManager sanity = SanityManager.getInstance(context); final FitbitBetaProbe me = this; final long now = System.currentTimeMillis(); if (token == null || refresh == null || token.trim().length() == 0 || refresh.trim().length() == 0) { String message = context.getString(R.string.message_fitbit_check); Runnable action = new Runnable() { @Override public void run() { me.fetchAuth(context); } }; sanity.addAlert(SanityCheck.WARNING, title, message, action); } else { final long expires = prefs.getLong(FitbitBetaProbe.OAUTH_TOKEN_EXPIRES, 0); Keystore.put(FitbitBetaApi.USER_ACCESS_TOKEN, prefs.getString(FitbitBetaProbe.OAUTH_ACCESS_TOKEN, null)); Keystore.put(FitbitBetaApi.USER_REFRESH_TOKEN, prefs.getString(FitbitBetaProbe.OAUTH_REFRESH_TOKEN, null)); Keystore.put(FitbitBetaApi.USER_TOKEN_EXPIRES, "" + expires); sanity.clearAlert(title); final String warningTitle = context.getString(R.string.config_probe_fitbit_rate_limit_warning_title); final String warningMessage = context.getString(R.string.config_probe_fitbit_rate_limit_warning); if (now - this._lastUpdate > 1000 * 60 * 15) { sanity.clearAlert(warningTitle); this._lastUpdate = now; Runnable r = new Runnable() { public void run() { try { if (System.currentTimeMillis() > expires) { FitbitBetaApi.refreshTokens(context, FitbitBetaProbe.OAUTH_ACCESS_TOKEN, FitbitBetaProbe.OAUTH_REFRESH_TOKEN, FitbitBetaProbe.OAUTH_TOKEN_EXPIRES); long expires = prefs.getLong(FitbitBetaProbe.OAUTH_TOKEN_EXPIRES, 0); Keystore.put(FitbitBetaApi.USER_ACCESS_TOKEN, prefs.getString(FitbitBetaProbe.OAUTH_ACCESS_TOKEN, null)); Keystore.put(FitbitBetaApi.USER_REFRESH_TOKEN, prefs.getString(FitbitBetaProbe.OAUTH_REFRESH_TOKEN, null)); Keystore.put(FitbitBetaApi.USER_TOKEN_EXPIRES, "" + expires); } int retrospectivePeriod = Integer.parseInt(prefs.getString(FitbitBetaProbe.RETROSPECTIVE_PERIOD, FitbitBetaProbe.DEFAULT_RETROSPECTIVE_PERIOD)); ArrayList<Long> minutesToEval = new ArrayList<>(); for(int i = 0; i < (retrospectivePeriod * 24 * 60); i++) minutesToEval.add((now / (60 * 1000)) - i); ArrayList<Long> minutesToSkip = new ArrayList<>(); JSONArray coveredRanges = new JSONArray(prefs.getString(FitbitBetaProbe.COVERED_RANGES, "[]")); MinuteRanges coveredMinutes = new MinuteRanges(); for (int i = 0; i < coveredRanges.length(); i++) { coveredMinutes.addRange(MinuteRange.rangeFromString(coveredRanges.getString(i))); } String coveredMinutesString = coveredMinutes.coalescedJSON(); for (long minute : minutesToEval) { if (coveredMinutes.containsMinute(minute)) minutesToSkip.add(minute); } minutesToEval.removeAll(minutesToSkip); HashSet<Long> evaledDays = new HashSet<>(); Calendar cal = Calendar.getInstance(); long lastMinute = -1; for (long minute : minutesToEval) { lastMinute = minute; if (evaledDays.contains(minute / (24 * 60))) { } else { cal.setTimeInMillis(minute * 60 * 1000); String month = "" + (cal.get(Calendar.MONTH) + 1); if (month.length() < 2) month = "0" + month; String monthDay = "" + cal.get(Calendar.DAY_OF_MONTH); if (monthDay.length() < 2) monthDay = "0" + monthDay; String dayString = cal.get(Calendar.YEAR) + "-" + month + "-" + monthDay; Bundle bundle = new Bundle(); bundle.putString(Probe.BUNDLE_PROBE, me.name(context)); bundle.putLong(Probe.BUNDLE_TIMESTAMP, System.currentTimeMillis() / 1000); boolean transmit = false; HashSet<Long> minutesToday = new HashSet<>(); if (prefs.getBoolean(FitbitBetaProbe.ENABLE_STEPS, FitbitBetaProbe.DEFAULT_ENABLE_STEPS)) { try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/steps/date/" + dayString + "/1d/1min.json")); JSONObject stepsIntraday = stepsObj.getJSONObject("activities-steps-intraday"); JSONArray stepsValues = stepsIntraday.getJSONArray("dataset"); ArrayList<Long> stepTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < stepsValues.length(); i++) { JSONObject value = stepsValues.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (coveredMinutes.containsMinute(minute) == false && valueQty > 0) { stepTimestamps.add(timestamp); valueList.add(valueQty); minutesToday.add(timestamp / (60 * 1000)); } } long[] timestamps = new long[stepTimestamps.size()]; long[] steps = new long[stepTimestamps.size()]; for (int i = 0; i < stepTimestamps.size(); i++) { timestamps[i] = stepTimestamps.get(i); steps[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.STEP_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.STEPS, steps); transmit = true; } catch (Exception e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (prefs.getBoolean(FitbitBetaProbe.ENABLE_CALORIES, FitbitBetaProbe.DEFAULT_ENABLE_CALORIES)) { long lastUpdate = prefs.getLong(FitbitBetaProbe.LAST_CALORIE_TIMESTAMP, 0); try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/calories/date/" + dayString + "/1d/1min.json")); JSONObject intraday = stepsObj.getJSONObject("activities-calories-intraday"); JSONArray valuesArray = intraday.getJSONArray("dataset"); ArrayList<Long> valueTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < valuesArray.length(); i++) { JSONObject value = valuesArray.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (timestamp > lastUpdate && valueQty > 0) { valueTimestamps.add(timestamp); lastUpdate = timestamp; valueList.add(valueQty); // Don't add to evaled since automatically calculated... } } long[] timestamps = new long[valueTimestamps.size()]; long[] values = new long[valueList.size()]; for (int i = 0; i < valueTimestamps.size(); i++) { timestamps[i] = valueTimestamps.get(i); values[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.CALORIES_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.CALORIES, values); Editor e = prefs.edit(); e.putLong(FitbitBetaProbe.LAST_CALORIE_TIMESTAMP, lastUpdate); e.commit(); transmit = true; } catch (Exception e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (prefs.getBoolean(FitbitBetaProbe.ENABLE_DISTANCE, FitbitBetaProbe.DEFAULT_ENABLE_DISTANCE)) { try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/distance/date/" + dayString + "/1d/1min.json")); JSONObject intraday = stepsObj.getJSONObject("activities-distance-intraday"); JSONArray valuesArray = intraday.getJSONArray("dataset"); ArrayList<Long> valueTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < valuesArray.length(); i++) { JSONObject value = valuesArray.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (coveredMinutes.containsMinute(minute) == false && valueQty > 0) { valueTimestamps.add(timestamp); valueList.add(valueQty); minutesToday.add(timestamp / (60 * 1000)); } } long[] timestamps = new long[valueTimestamps.size()]; long[] values = new long[valueList.size()]; for (int i = 0; i < valueTimestamps.size(); i++) { timestamps[i] = valueTimestamps.get(i); values[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.DISTANCE_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.DISTANCE, values); transmit = true; } catch (Exception e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (prefs.getBoolean(FitbitBetaProbe.ENABLE_FLOORS, FitbitBetaProbe.DEFAULT_ENABLE_FLOORS)) { try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/floors/date/" + dayString + "/1d/1min.json")); JSONObject intraday = stepsObj.getJSONObject("activities-floors-intraday"); JSONArray valuesArray = intraday.getJSONArray("dataset"); ArrayList<Long> valueTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < valuesArray.length(); i++) { JSONObject value = valuesArray.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (coveredMinutes.containsMinute(minute) == false && valueQty > 0) { valueTimestamps.add(timestamp); valueList.add(valueQty); minutesToday.add(timestamp / (60 * 1000)); } } long[] timestamps = new long[valueTimestamps.size()]; long[] values = new long[valueList.size()]; for (int i = 0; i < valueTimestamps.size(); i++) { timestamps[i] = valueTimestamps.get(i); values[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.FLOORS_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.FLOORS, values); transmit = true; } catch (JSONException e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (prefs.getBoolean(FitbitBetaProbe.ENABLE_HEART, FitbitBetaProbe.DEFAULT_ENABLE_HEART)) { try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/heart/date/" + dayString + "/1d/1min.json")); JSONObject intraday = stepsObj.getJSONObject("activities-heart-intraday"); JSONArray valuesArray = intraday.getJSONArray("dataset"); ArrayList<Long> valueTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < valuesArray.length(); i++) { JSONObject value = valuesArray.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (coveredMinutes.containsMinute(minute) == false && valueQty > 0) { valueTimestamps.add(timestamp); valueList.add(valueQty); minutesToday.add(timestamp / (60 * 1000)); } } long[] timestamps = new long[valueTimestamps.size()]; long[] values = new long[valueList.size()]; for (int i = 0; i < valueTimestamps.size(); i++) { timestamps[i] = valueTimestamps.get(i); values[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.HEART_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.HEART, values); transmit = true; } catch (JSONException e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (prefs.getBoolean(FitbitBetaProbe.ENABLE_ELEVATION, FitbitBetaProbe.DEFAULT_ENABLE_ELEVATION)) { try { JSONObject stepsObj = FitbitBetaApi.fetch(Uri.parse("https://api.fitbit.com/1/user/-/activities/elevation/date/" + dayString + "/1d/1min.json")); JSONObject intraday = stepsObj.getJSONObject("activities-elevation-intraday"); JSONArray valuesArray = intraday.getJSONArray("dataset"); ArrayList<Long> valueTimestamps = new ArrayList<>(); ArrayList<Long> valueList = new ArrayList<>(); Calendar c = Calendar.getInstance(); for (int i = 0; i < valuesArray.length(); i++) { JSONObject value = valuesArray.getJSONObject(i); String time = value.getString("time"); String[] tokens = time.split(":"); c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(tokens[0])); c.set(Calendar.MINUTE, Integer.parseInt(tokens[1])); c.set(Calendar.SECOND, Integer.parseInt(tokens[2])); c.set(Calendar.MILLISECOND, 0); long timestamp = c.getTimeInMillis(); long valueQty = value.getLong("value"); if (coveredMinutes.containsMinute(minute) == false && valueQty > 0) { valueTimestamps.add(timestamp); valueList.add(valueQty); minutesToday.add(timestamp / (60 * 1000)); } } long[] timestamps = new long[valueTimestamps.size()]; long[] values = new long[valueList.size()]; for (int i = 0; i < valueTimestamps.size(); i++) { timestamps[i] = valueTimestamps.get(i); values[i] = valueList.get(i); } bundle.putLongArray(FitbitBetaProbe.ELEVATION_TIMESTAMPS, timestamps); bundle.putLongArray(FitbitBetaProbe.ELEVATION, values); transmit = true; } catch (JSONException e) { e.printStackTrace(); me._lastUpdate = System.currentTimeMillis() + (1000 * 60 * 15); SanityManager.getInstance(context).addAlert(SanityCheck.WARNING, warningTitle, warningMessage, null); break; } } if (transmit) { me.transmitData(context, bundle); } evaledDays.add(minute / (24 * 60)); for (Long todayMinute : minutesToday) { coveredMinutes.addMinute(todayMinute); } } } long firstMinute = (now / (60 * 1000)) - (5 * 24 * 60); if (lastMinute < firstMinute) { coveredMinutes.addRange(new MinuteRange(lastMinute, firstMinute)); } String coalescedRanges = coveredMinutes.coalescedJSON(); Editor e = prefs.edit(); e.putString(FitbitBetaProbe.COVERED_RANGES, coalescedRanges); e.commit(); } catch (Exception e) { e.printStackTrace(); } } }; Thread t = new Thread(r); t.start(); } } return true; } } return false; } private void fetchAuth(Context context) { this.initKeystore(context); String userId = EncryptionManager.getInstance().getUserHash(context); Intent intent = new Intent(context, OAuthActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(OAuthActivity.CONSUMER_KEY, context.getString(R.string.fitbit_consumer_key)); intent.putExtra(OAuthActivity.CONSUMER_SECRET, context.getString(R.string.fitbit_consumer_secret)); intent.putExtra(OAuthActivity.REQUESTER, "fitbit-beta"); intent.putExtra(OAuthActivity.CALLBACK_URL, "http://purple.robot.com/oauth/fitbit-beta"); intent.putExtra(OAuthActivity.LOG_URL, LogManager.getInstance(context).getLogUrl(context)); intent.putExtra(OAuthActivity.HASH_SECRET, userId); context.startActivity(intent); } @Override public JSONObject fetchSettings(Context context) { JSONObject settings = super.fetchSettings(context); try { JSONObject enabled = new JSONObject(); enabled.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); JSONArray values = new JSONArray(); values.put(true); values.put(false); enabled.put(Probe.PROBE_VALUES, values); settings.put(FitbitBetaProbe.ENABLE_STEPS, enabled); settings.put(FitbitBetaProbe.ENABLE_DISTANCE, enabled); settings.put(FitbitBetaProbe.ENABLE_CALORIES, enabled); settings.put(FitbitBetaProbe.ENABLE_ELEVATION, enabled); settings.put(FitbitBetaProbe.ENABLE_HEART, enabled); settings.put(FitbitBetaProbe.ENABLE_FLOORS, enabled); JSONObject retro = new JSONObject(); retro.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_STRING); JSONArray retroValues = new JSONArray(); retroValues.put("1"); retroValues.put("3"); retroValues.put("5"); retroValues.put("7"); retroValues.put("14"); retroValues.put("30"); retroValues.put("60"); retroValues.put("90"); retro.put(Probe.PROBE_VALUES, retroValues); settings.put(FitbitBetaProbe.RETROSPECTIVE_PERIOD, retro); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } return settings; } @Override public Map<String, Object> configuration(Context context) { Map<String, Object> map = super.configuration(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); map.put(FitbitBetaProbe.ENABLE_CALORIES, prefs.getBoolean(FitbitBetaProbe.ENABLE_CALORIES, FitbitBetaProbe.DEFAULT_ENABLE_CALORIES)); map.put(FitbitBetaProbe.ENABLE_STEPS, prefs.getBoolean(FitbitBetaProbe.ENABLE_STEPS, FitbitBetaProbe.DEFAULT_ENABLE_STEPS)); map.put(FitbitBetaProbe.ENABLE_HEART, prefs.getBoolean(FitbitBetaProbe.ENABLE_HEART, FitbitBetaProbe.DEFAULT_ENABLE_HEART)); map.put(FitbitBetaProbe.ENABLE_DISTANCE, prefs.getBoolean(FitbitBetaProbe.ENABLE_DISTANCE, FitbitBetaProbe.DEFAULT_ENABLE_DISTANCE)); map.put(FitbitBetaProbe.ENABLE_ELEVATION, prefs.getBoolean(FitbitBetaProbe.ENABLE_ELEVATION, FitbitBetaProbe.DEFAULT_ENABLE_ELEVATION)); map.put(FitbitBetaProbe.FLOORS, prefs.getBoolean(FitbitBetaProbe.FLOORS, FitbitBetaProbe.DEFAULT_ENABLE_FLOORS)); map.put(FitbitBetaProbe.RETROSPECTIVE_PERIOD, prefs.getString(FitbitBetaProbe.RETROSPECTIVE_PERIOD, FitbitBetaProbe.DEFAULT_RETROSPECTIVE_PERIOD)); return map; } @Override public void updateFromMap(Context context, Map<String, Object> params) { super.updateFromMap(context, params); String[] keys = { FitbitBetaProbe.ENABLE_CALORIES, FitbitBetaProbe.ENABLE_STEPS, FitbitBetaProbe.ENABLE_DISTANCE, FitbitBetaProbe.ENABLE_HEART }; for (String key: keys) { if (params.containsKey(key)) { Object value = params.get(key); if (value instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(key, ((Boolean) value)); e.commit(); } } } if (params.containsKey(FitbitBetaProbe.RETROSPECTIVE_PERIOD)) { Object value = params.get(FitbitBetaProbe.RETROSPECTIVE_PERIOD); if (value instanceof String) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putString(FitbitBetaProbe.RETROSPECTIVE_PERIOD, value.toString()); e.commit(); } } } @Override public PreferenceScreen preferenceScreen(final Context context, PreferenceManager manager) { final PreferenceScreen screen = manager.createPreferenceScreen(context); screen.setTitle(this.title(context)); screen.setSummary(this.summary(context)); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_probe); enabled.setKey(FitbitBetaProbe.ENABLED); enabled.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLED); screen.addPreference(enabled); CheckBoxPreference stepsPref = new CheckBoxPreference(context); stepsPref.setKey(FitbitBetaProbe.ENABLE_STEPS); stepsPref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_STEPS); stepsPref.setTitle(R.string.config_fitbit_steps_title); screen.addPreference(stepsPref); CheckBoxPreference distancePref = new CheckBoxPreference(context); distancePref.setKey(FitbitBetaProbe.ENABLE_DISTANCE); distancePref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_DISTANCE); distancePref.setTitle(R.string.config_fitbit_distance_title); screen.addPreference(distancePref); CheckBoxPreference floorsPref = new CheckBoxPreference(context); floorsPref.setKey(FitbitBetaProbe.FLOORS); floorsPref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_FLOORS); floorsPref.setTitle(R.string.config_fitbit_floors_title); screen.addPreference(floorsPref); CheckBoxPreference elevationPref = new CheckBoxPreference(context); elevationPref.setKey(FitbitBetaProbe.ENABLE_ELEVATION); elevationPref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_ELEVATION); elevationPref.setTitle(R.string.config_fitbit_elevation_title); screen.addPreference(elevationPref); CheckBoxPreference caloriesPref = new CheckBoxPreference(context); caloriesPref.setKey(FitbitBetaProbe.ENABLE_CALORIES); caloriesPref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_CALORIES); caloriesPref.setTitle(R.string.config_fitbit_calories_title); screen.addPreference(caloriesPref); CheckBoxPreference heartPref = new CheckBoxPreference(context); heartPref.setKey(FitbitBetaProbe.ENABLE_HEART); heartPref.setDefaultValue(FitbitBetaProbe.DEFAULT_ENABLE_HEART); heartPref.setTitle(R.string.config_fitbit_heart_title); screen.addPreference(heartPref); FlexibleListPreference retrospective = new FlexibleListPreference(context); retrospective.setKey(FitbitBetaProbe.RETROSPECTIVE_PERIOD); retrospective.setDefaultValue(FitbitBetaProbe.DEFAULT_RETROSPECTIVE_PERIOD); retrospective.setEntryValues(R.array.probe_fitbit_retrospective_values); retrospective.setEntries(R.array.probe_fitbit_retrospective_labels); retrospective.setTitle(R.string.probe_fitbit_retrospective_label); screen.addPreference(retrospective); final SharedPreferences prefs = Probe.getPreferences(context); String token = prefs.getString(FitbitBetaProbe.OAUTH_ACCESS_TOKEN, null); String refresh = prefs.getString(FitbitBetaProbe.OAUTH_REFRESH_TOKEN, null); final Preference authPreference = new Preference(context); authPreference.setTitle(R.string.title_authenticate_fitbit_probe); authPreference.setSummary(R.string.summary_authenticate_fitbit_probe); final Preference logoutPreference = new Preference(context); logoutPreference.setTitle(R.string.title_logout_fitbit_probe); logoutPreference.setSummary(R.string.summary_logout_fitbit_probe); final FitbitBetaProbe me = this; authPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { me.fetchAuth(context); screen.addPreference(logoutPreference); screen.removePreference(authPreference); return true; } }); logoutPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { Editor e = prefs.edit(); e.remove(FitbitBetaProbe.OAUTH_ACCESS_TOKEN); e.remove(FitbitBetaProbe.OAUTH_REFRESH_TOKEN); e.remove(FitbitBetaProbe.OAUTH_TOKEN_EXPIRES); e.commit(); me._lastUpdate = 0; screen.addPreference(authPreference); screen.removePreference(logoutPreference); if (context instanceof Activity) { Activity activity = (Activity) context; activity.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(context, context.getString(R.string.toast_fitbit_logout), Toast.LENGTH_LONG).show(); } }); } return true; } }); if (token == null || refresh == null) screen.addPreference(authPreference); else screen.addPreference(logoutPreference); return screen; } @Override public String summarizeValue(Context context, Bundle bundle) { if (bundle.containsKey(FitbitBetaProbe.CALORIES_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.CALORIES_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } else if (bundle.containsKey(FitbitBetaProbe.DISTANCE_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.DISTANCE_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } else if (bundle.containsKey(FitbitBetaProbe.ELEVATION_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.ELEVATION_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } else if (bundle.containsKey(FitbitBetaProbe.FLOORS_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.FLOORS_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } else if (bundle.containsKey(FitbitBetaProbe.HEART_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.HEART_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } else if (bundle.containsKey(FitbitBetaProbe.STEP_TIMESTAMPS)) { double[] timestamps = bundle.getDoubleArray(FitbitBetaProbe.STEP_TIMESTAMPS); return String.format(context.getResources().getString(R.string.summary_fitbit_beta), timestamps.length); } return context.getString(R.string.summary_fitbit_beta_no_datapoints); } }